Skip to content

MIT S081 Lab 11: mmap

前置知识

什么是mmap

实验描述

可以通过多种方式调用mmap,但本实验只需要与内存映射文件相关的功能子集。您可以假设addr始终为零,这意味着内核应该决定映射文件的虚拟地址。mmap返回该地址,如果失败则返回0xfffffffffffffffflength是要映射的字节数;它可能与文件的长度不同。prot指示内存是否应映射为可读、可写,以及/或者可执行的;您可以认为protPROT_READPROT_WRITE或两者兼有。flags要么是MAP_SHARED(映射内存的修改应写回文件),要么是MAP_PRIVATE(映射内存的修改不应写回文件)。您不必在flags中实现任何其他位。fd是要映射的文件的打开文件描述符。可以假定offset为零(它是要映射的文件的起点)。

允许进程映射同一个MAP_SHARED文件而不共享物理页面。

munmap(addr, length)应删除指定地址范围内的mmap映射。如果进程修改了内存并将其映射为MAP_SHARED,则应首先将修改写入文件。munmap调用可能只覆盖mmap区域的一部分,但您可以认为它取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域(但不会在区域中间“打洞”)。

博客描述

内存映射mmap:原理、优缺点与适用场景-CSDN博客

课程中的介绍

通过上面的系统调用,可以将文件描述符指向的文件内容,从起始位置加上offset的地方开始,映射到特定的内存地址(如果指定了的话),并且连续映射len长度。这使得你可以实现Memory Mapped File,你可以将文件的内容带到内存地址空间,进而只需要方便的通过普通的指针操作,而不用调用read/write系统调用,就可以从磁盘读写文件内容。这是一个方便的接口,可以用来操纵存储在文件中的数据结构。实际上,你们将会在下个lab实现基于文件的mmap,下个lab结合了XV6的文件系统和虚拟内存,进而实现mmap。 讲的不是很懂,可能边做实验边看才能懂这什么东西。

VMA(虚拟内存区域)

提示三提到的内容在这里:17.2 支持应用程序使用虚拟内存的系统调用 | MIT6.S081

步骤

1、添加系统调用接口

2、声明VMA结构体

c

#define NVMA 16
struct VMA
{
  int used;
  uint64 addr;        // 起始地址
  int len;            // 长度
  int prot;           // 权限
  int flags;          // 标志位
  int vfd;            // 对应的文件描述符
  struct file* vfile; // 对应文件
  int offset;         // 文件偏移,本实验中一直为0
};
//并添加到proc里面
struct proc
{
 . . .
 struct VMA vma[NVMA]
}

3、初始化VMA

allocproc函数中分配空间,

c
static struct proc*
allocproc(void)
{
  . . .
  //initialize vma to 0.
  memset(&p->vma,0,sizeof(&p->vma));
  return p;
}

并在mmap函数中增加VMA中文件指针的引用。%% mmap应该增加文件的引用计数,以便在文件关闭时结构体不会消失(提示:请参阅filedup)。运行mmaptest:第一次mmap应该成功,但是第一次访问被mmap的内存将导致缺页异常并终止mmaptest。 %% mmap需要正常读取参数,并根据提示判断addr、offset是否为0,判断读写权限,是否溢出等.

c
sys_mmap(void)
{
 uint64 addr;
  int length;
  int prot;
  int flags;
  int vfd;
  struct file* vfile;
  int offset;
  uint64 err = 0xffffffffffffffff;


  // 获取系统调用参数
  if(argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 ||
    argint(3, &flags) < 0 || argfd(4, &vfd, &vfile) < 0 || argint(5, &offset) < 0)
    return err;
  
  // 实验提示中假定addr和offset为0,简化程序可能发生的情况
  if(addr != 0 || offset != 0 || length < 0)
    return err;
  // 文件不可写则不允许拥有PROT_WRITE权限时映射为MAP_SHARED
  if(vfile->writable == 0 && (prot & PROT_WRITE) != 0 && flags == MAP_SHARED)
    return err;
  struct proc* p = myproc();
  // 没有足够的虚拟地址空间
  if(p->sz + length > MAXVA)
    return err;
  
  // 遍历查找未使用的VMA结构体
  for(int i = 0; i < NVMA; ++i) {
    if(p->vma[i].used == 0) {
      p->vma[i].used = 1;
      p->vma[i].addr = p->sz;
      p->vma[i].len = length;
      p->vma[i].flags = flags;
      p->vma[i].prot = prot;
      p->vma[i].vfile = vfile;
      p->vma[i].vfd = vfd;
      p->vma[i].offset = offset;
  
      // 增加文件的引用计数
      filedup(vfile);
  
      p->sz += length;
      return p->vma[i].addr;
    }
  }

  return err;
}

运行mmaptest:第一次mmap应该成功,但是第一次访问被mmap的内存将导致缺页异常并终止mmaptest为什么第一次成功,访问mmap的内存发生什么事了?此时只完成了映射到虚拟内存的工作,第一步并没有为它分配内存,所以尝试访问内存会发生缺页异常。

4、处理缺页异常,在缺页时为虚拟页面分配一页的空间。

  • 提示: 添加代码以导致在mmap的区域中产生缺页异常,从而分配一页物理内存,将4096字节的相关文件读入该页面,并将其映射到用户地址空间。使用readi读取文件,它接受一个偏移量参数,在该偏移处读取文件(但必须lock/unlock传递给readi的索引结点)。不要忘记在页面上正确设置权限。运行mmaptest;它应该到达第一个munmap
  • 类似trap的调用,在usertrap里面添加处理page fault的情况,然后在添加处理。
c
else if(r_scause() == 13 || r_scause() == 15) {
    #ifdef LAB_MMAP
        // 读取产生缺页故障的虚拟地址,并判断是否位于有效区间
        uint64 fault_va = r_stval();
        if(PGROUNDUP(p->trapframe->sp) - 1 < fault_va && fault_va < p->sz) {
          if(mmap_handler(r_stval(), r_scause()) != 0) p->killed = 1;
        } else
          p->killed = 1;
    #endif
      } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }
  • 使用的辅助函数mmap_handler()来进行处理
    • 分配物理页面
    • 读取文件内容 (注意添加不同标志位)
    • 添加映射关系
c
int mmap_handler(int va, int cause) {
  int i;
  struct proc* p = myproc();
  // 根据地址查找属于哪一个VMA
  for(i = 0; i < NVMA; ++i) {
    if(p->vma[i].used && p->vma[i].addr <= va && va <= p->vma[i].addr + p->vma[i].len - 1) {
      break;
    }
  }
  if(i == NVMA)
    return -1;
  
  int pte_flags = PTE_U;
  if(p->vma[i].prot & PROT_READ) pte_flags |= PTE_R;
  if(p->vma[i].prot & PROT_WRITE) pte_flags |= PTE_W;
  if(p->vma[i].prot & PROT_EXEC) pte_flags |= PTE_X;
  
 
  struct file* vf = p->vma[i].vfile;
  // 读导致的缺页异常
  if(cause == 13 && vf->readable == 0) return -1;
  // 写导致的缺页异常
  if(cause == 15 && vf->writable == 0) return -1;

 
  void* pa = kalloc();
  if(pa == 0)
    return -1;
  memset(pa, 0, PGSIZE);
  
  // 读取文件内容
  ilock(vf->ip);
  // 计算当前页面读取文件的偏移量,实验中p->vma[i].offset总是0
  // 要按顺序读读取,例如内存页面A,B和文件块a,b
  // 则A读取a,B读取b,而不能A读取b,B读取a
  int offset = p->vma[i].offset + PGROUNDDOWN(va - p->vma[i].addr);
  int readbytes = readi(vf->ip, 0, (uint64)pa, offset, PGSIZE);
  // 什么都没有读到

  if(readbytes == 0) {
    iunlock(vf->ip);
    kfree(pa);
    return -1;
  }
  iunlock(vf->ip);
  
  // 添加页面映射
  if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, pte_flags) != 0) {
    kfree(pa);
    return -1;
  }
  
  return 0;
}

5、根据提示6实现munmap

%%找到地址范围的VMA并取消映射指定页面(提示:使用uvmunmap)。如果munmap删除了先前mmap的所有页面,它应该减少相应struct file的引用计数。如果未映射的页面已被修改,并且文件已映射到MAP_SHARED,请将页面写回该文件。查看filewrite以获得灵感。%% 提示7中说明无需查看脏位就可写回%%- 理想情况下,您的实现将只写回程序实际修改的MAP_SHARED页面。RISC-V PTE中的脏位(D)表示是否已写入页面。但是,mmaptest不检查非脏页是否没有回写;因此,您可以不用看D位就写回页面。%%

c
uint64
sys_munmap(void)
{
  uint64 addr;
  int length;
  if(argaddr(0, &addr) < 0 || argint(1, &length) < 0)
    return -1;
  
  int i;
  struct proc* p = myproc();
  for(i = 0; i < NVMA; ++i) {
    if(p->vma[i].used && p->vma[i].len >= length) {
      // 根据提示,munmap的地址范围只能是
      // 1. 起始位置
      if(p->vma[i].addr == addr) {
        p->vma[i].addr += length;
        p->vma[i].len -= length;
        break;
      }
      // 2. 结束位置
      if(addr + length == p->vma[i].addr + p->vma[i].len) {
        p->vma[i].len -= length;
        break;
      }
    }
  }
  if(i == NVMA)
    return -1;
  
  // 将MAP_SHARED页面写回文件系统
  if(p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0) {
    filewrite(p->vma[i].vfile, addr, length);
  }
  
  // 判断此页面是否存在映射
  uvmunmap(p->pagetable, addr, length / PGSIZE, 1);
  
 
  // 当前VMA中全部映射都被取消
  if(p->vma[i].len == 0) {
    fileclose(p->vma[i].vfile);
    p->vma[i].used = 0;
  }
  
  return 0;
}

6、 修改exit将进程的已映射区域取消映射

%%就像调用了munmap一样。运行mmaptestmmap_test应该通过,但可能不会通过fork_test%%添加下面这段代码

c
    // 将进程的已映射区域取消映射
   for(int i = 0; i < NVMA; ++i) {
     if(p->vma[i].used) {
       if(p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0) {
         filewrite(p->vma[i].vfile, p->vma[i].addr, p->vma[i].len);
       }
       fileclose(p->vma[i].vfile);
       uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].len / PGSIZE, 1);
       p->vma[i].used = 0;
     }
   }

7、 修改fork

%%以确保子对象具有与父对象相同的映射区域。不要忘记增加VMA的struct file的引用计数。在子进程的页面错误处理程序中,可以分配新的物理页面,而不是与父级共享页面。后者会更酷,但需要更多的实施工作。运行mmaptest;它应该通过mmap_testfork_test。%%添加下面的代码。

c
 // 复制父进程的VMA
  for(i = 0; i < NVMA; ++i) {
   if(p->vma[i].used) {
      memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));
      filedup(p->vma[i].vfile);
    }
  }

/**

  • Page Fault / else if (r_scause() == 13 || r_scause() == 15) { / printf("page fault!\n"); printf("-------------------------before page table:\n"); vmprint(p->pagetable, 1); printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid); printf(" sepc=%p stval=%p\n", r_sepc(), r_stval()); */ //char *mem; uint64 va;
va = r_stval();
/* printf("size alloc:%d\n", p->sz);
printf("va:%d\n", va); */
if(va >= p->sz)
{

  printf("Virtual Address is greater than sbrk(n) \n");
  p->killed = 1;
}
else {

  uint64 va_boundry = PGROUNDDOWN(va);
  mem = kalloc();
  if(mem != 0){
    memset(mem, 0, PGSIZE);
    //printf("%p\n", (uint64)mem);
 
    if(mappages(p->pagetable, va_boundry, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
      printf("Cannot allocate so much memory");
      kfree(mem);
      uvmdealloc(p->pagetable, va_boundry, p->sz);
      p->killed = 1;
    }
    //printf("%p\n", (uint64)mem);
    //printf("-------------------------after page table:\n");
    //vmprint(p->pagetable, 1);
    //printf("map:%p\n", walkaddr(p->pagetable, va)); 
  }
  else{
    printf("Mem is 0 \n");
    p->killed = 1;
  } 
}

} else { printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid); printf(" sepc=%p stval=%p\n", r_sepc(), r_stval()); p->killed = 1; }

上次更新于: